package com.demo.system.service.impl;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 自定义WebSocket处理器类
 * <p>
 * 这个类继承自TextWebSocketHandler，是Spring WebSocket框架中处理文本消息的核心处理器。
 * 主要负责：
 * 1. 管理WebSocket连接的生命周期（连接建立、断开、错误处理）
 * 2. 处理客户端发送的文本消息
 * 3. 实现消息的广播和单播功能
 * 4. 维护在线用户会话状态
 *
 * @author why
 * @description WebSocket消息处理核心类，实现聊天室功能
 * @date 2025/9/23 10:51
 */
@Slf4j  // Lombok注解，自动生成日志对象
@Component  // Spring组件注解，标记为Bean并纳入容器管理
public class MyWebSocketHandler extends TextWebSocketHandler {

    /**
     * 用户会话映射表
     * <p>
     * 使用ConcurrentHashMap保证线程安全，key为用户ID，value为对应的WebSocket会话。
     * 这个Map用于：
     * 1. 跟踪当前在线的用户
     * 2. 根据用户ID快速查找对应的WebSocket会话
     * 3. 实现消息的定向发送和广播功能
     */
    private static final Map<String, WebSocketSession> userSessions = new ConcurrentHashMap<>();

    /**
     * 处理Pong消息
     * <p>
     * Pong消息是WebSocket协议中的心跳响应消息，用于保持连接活跃。
     * 当客户端收到Ping消息后会自动发送Pong消息作为响应。
     *
     * @param session WebSocket会话对象
     * @param message Pong消息对象
     * @throws Exception 处理异常
     */
    @Override
    protected void handlePongMessage(WebSocketSession session, PongMessage message) throws Exception {
        super.handlePongMessage(session, message);
    }

    /**
     * 处理通用WebSocket消息
     * <p>
     * 这是所有WebSocket消息的入口方法，会根据消息类型分发到具体的处理方法。
     * 目前主要处理文本消息，其他类型消息交由父类处理。
     *
     * @param session WebSocket会话对象
     * @param message WebSocket消息对象（可能是文本、二进制、关闭、Ping、Pong等类型）
     * @throws Exception 处理异常
     */
    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        super.handleMessage(session, message);
    }

    /**
     * WebSocket连接建立后的回调方法
     * <p>
     * 当客户端成功建立WebSocket连接时，Spring框架会自动调用此方法。
     * 这是WebSocket生命周期的重要节点，主要完成以下工作：
     * 1. 从连接URI中提取用户标识
     * 2. 将用户会话加入到在线用户管理Map中
     * 3. 向客户端发送连接成功的确认消息
     * 4. 记录连接建立的日志信息
     *
     * @param session WebSocket会话对象，包含连接信息和通信能力
     * @throws Exception 连接处理过程中可能抛出的异常
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 获取连接的完整URI路径
        String uri = session.getUri().toString();
        log.info("session established: " + session.toString());
        // 从URI中解析出用户ID（如：/ws/chat/user_123 -> user_123）
        String userId = extractUserIdFromUri(uri);

        if (userId != null) {
            // 将用户会话存储到管理Map中，实现用户ID与会话的映射关系
            userSessions.put(userId, session);
            log.info("用户 {} 连接WebSocket成功，当前在线用户数: {}", userId, userSessions.size());

            // 向客户端发送连接成功的确认消息
            // 消息格式为JSON，包含连接类型、客户端ID和成功消息
            try {
                String welcomeMessage = String.format(
                        "{\"type\":\"connection_success\",\"clientId\":\"%s\",\"message\":\"连接成功\"}",
                        userId
                );
                session.sendMessage(new TextMessage(welcomeMessage));
            } catch (Exception e) {
                log.error("发送欢迎消息失败", e);
            }
        } else {
            // 如果无法提取用户ID，记录警告日志
            log.warn("无法从URI中提取用户ID: {}", uri);
        }

        // 调用父类方法，完成标准的连接建立流程
        super.afterConnectionEstablished(session);
    }

    /**
     * WebSocket连接关闭后的回调方法
     * <p>
     * 当WebSocket连接因各种原因关闭时（客户端主动断开、网络异常、服务器关闭等），
     * Spring框架会自动调用此方法进行清理工作。主要完成：
     * 1. 根据会话查找对应的用户ID
     * 2. 从在线用户Map中移除该用户
     * 3. 向其他在线用户广播用户离开消息
     * 4. 更新并广播最新的在线用户数量
     *
     * @param session WebSocket会话对象
     * @param status  连接关闭状态，包含关闭原因和状态码
     * @throws Exception 处理过程中可能抛出的异常
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        // 根据会话对象反向查找对应的用户ID
        String userId = findUserIdBySession(session);
        if (userId != null) {
            // 从在线用户管理Map中移除该用户
            userSessions.remove(userId);
            log.info("用户 {} 断开WebSocket连接，当前在线用户数: {}", userId, userSessions.size());

            // 向所有其他在线用户广播用户离开消息和更新在线用户数
            try {
                // 构造用户离开消息的JSON格式
                String userLeaveMessage = String.format(
                        "{\"type\":\"user_leave\",\"user\":{\"id\":\"%s\",\"username\":\"用户%s\"}}",
                        userId, userId
                );
                broadcastMessage(userLeaveMessage);

                // 构造在线用户数更新消息的JSON格式
                String onlineUsersMessage = String.format(
                        "{\"type\":\"online_users\",\"count\":%d}",
                        userSessions.size()
                );
                broadcastMessage(onlineUsersMessage);
            } catch (Exception e) {
                log.error("发送用户离开消息失败", e);
            }
        }
        // 调用父类方法，完成标准的连接关闭流程
        super.afterConnectionClosed(session, status);
    }

    /**
     * 处理客户端发送的文本消息
     * <p>
     * 这是WebSocket文本消息处理的核心方法，当客户端发送文本消息时会自动调用。
     * 支持两种消息格式：
     * 1. JSON格式消息：包含type字段，根据不同类型执行不同逻辑
     * - user_join: 用户加入聊天室，返回在线用户数
     * - message: 聊天消息，广播给其他用户
     * - 其他类型: 统一广播处理
     * 2. 普通文本消息：直接广播给其他用户
     *
     * @param session WebSocket会话对象，标识消息发送者
     * @param message 文本消息对象，包含消息内容
     * @throws Exception 消息处理过程中可能抛出的异常
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 根据会话查找发送消息的用户ID
        String userId = findUserIdBySession(session);
        // 获取消息的实际内容
        String payload = message.getPayload();
        log.info("收到用户 {} 的消息: {}", userId, payload);

        try {
            // 判断消息格式：检查是否为JSON格式的消息
            if (payload.startsWith("{") && payload.endsWith("}")) {
                // JSON格式消息处理：根据type字段进行不同的业务逻辑处理
                if (payload.contains("\"type\":\"user_join\"")) {
                    // 用户加入聊天室的处理逻辑
                    log.info("用户 {} 发送加入消息", userId);
                    // 构造并广播当前在线用户数量信息
                    String onlineUsersMessage = String.format(
                            "{\"type\":\"online_users\",\"count\":%d}",
                            userSessions.size()
                    );
                    broadcastMessage(onlineUsersMessage);

                } else if (payload.contains("\"type\":\"message\"")) {
                    // 聊天消息的处理逻辑
                    log.info("用户 {} 发送聊天消息", userId);
                    // 将聊天消息广播给所有其他在线用户（排除发送者自己）
                    broadcastMessageExcludeSender(payload, session);

                } else {
                    // 其他类型JSON消息的统一处理
                    log.info("收到其他类型消息: {}", payload);
                    // 统一广播给其他用户
                    broadcastMessageExcludeSender(payload, session);
                }
            } else {
                // 普通文本消息处理：直接广播给其他用户
                log.info("收到文本消息: {}", payload);
                broadcastMessageExcludeSender(payload, session);
            }

        } catch (Exception e) {
            log.error("处理消息时发生错误", e);
        }

        // 调用父类方法，完成标准的文本消息处理流程
        super.handleTextMessage(session, message);
    }

    /**
     * 处理WebSocket传输错误
     * <p>
     * 当WebSocket连接发生传输层错误时（如网络中断、协议错误等），
     * Spring框架会自动调用此方法进行错误处理。
     * 主要用于记录错误日志，便于问题排查和系统监控。
     *
     * @param session   WebSocket会话对象
     * @param exception 发生的异常对象，包含错误详细信息
     * @throws Exception 错误处理过程中可能抛出的异常
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        // 查找发生错误的用户ID，便于日志记录和问题定位
        String userId = findUserIdBySession(session);
        log.error("用户 {} WebSocket连接发生错误", userId, exception);
        // 调用父类方法，完成标准的错误处理流程
        super.handleTransportError(session, exception);
    }

    /**
     * 从WebSocket连接URI中提取用户标识
     * <p>
     * 解析WebSocket连接的URI路径，提取其中的用户ID信息。
     * 支持多种URI格式，如：/ws/chat/user_123、/api/ws/chat/user_456等。
     * 采用多重解析策略确保能够正确提取用户ID。
     *
     * @param uri WebSocket连接的完整URI字符串
     * @return 提取到的用户ID，如果提取失败则返回"unknown_user"
     */
    private String extractUserIdFromUri(String uri) {
        log.debug("提取用户ID从URI: {}", uri);
        try {
            // 主要解析策略：从类似 /api/ws/chat/user_10 的URI中提取用户ID
            String[] parts = uri.split("/");

            // 遍历URI路径片段，查找"chat"关键字后的用户ID
            for (int i = 0; i < parts.length; i++) {
                if ("chat".equals(parts[i]) && i + 1 < parts.length) {
                    String userId = parts[i + 1];
                    // 移除可能的查询参数（如：user_123?token=abc -> user_123）
                    if (userId.contains("?")) {
                        userId = userId.substring(0, userId.indexOf("?"));
                    }
                    log.debug("提取到用户ID: {}", userId);
                    return userId;
                }
            }

            // 备用解析策略：取URI路径中最后一个有效的非关键字部分作为用户ID
            // 这种策略适用于非标准格式的URI路径
            for (int i = parts.length - 1; i >= 0; i--) {
                String part = parts[i];
                // 过滤掉空字符串和系统关键字
                if (!part.isEmpty() && !"chat".equals(part) && !"ws".equals(part) && !"api".equals(part)) {
                    String userId = part;
                    // 移除查询参数
                    if (userId.contains("?")) {
                        userId = userId.substring(0, userId.indexOf("?"));
                    }
                    log.debug("备用提取到用户ID: {}", userId);
                    return userId;
                }
            }
        } catch (Exception e) {
            log.error("提取用户ID失败", e);
        }
        // 如果所有解析策略都失败，返回默认的未知用户标识
        return "unknown_user";
    }

    /**
     * 根据WebSocket会话反向查找用户ID
     * <p>
     * 在用户会话映射表中根据WebSocketSession对象查找对应的用户ID。
     * 这个方法主要用于连接断开、错误处理等场景，当只有session对象时
     * 需要确定对应的用户身份。
     *
     * @param session WebSocket会话对象
     * @return 对应的用户ID，如果未找到则返回null
     */
    private String findUserIdBySession(WebSocketSession session) {
        return userSessions.entrySet().stream()
                .filter(entry -> entry.getValue().equals(session))  // 查找匹配的会话
                .map(Map.Entry::getKey)  // 提取用户ID
                .findFirst()  // 获取第一个匹配结果
                .orElse(null);  // 未找到时返回null
    }

    /**
     * 向指定用户发送消息（单播）
     * <p>
     * 根据用户ID查找对应的WebSocket会话，并向该用户发送指定消息。
     * 这是点对点消息发送的实现，常用于私聊、系统通知等场景。
     *
     * @param userId  目标用户的ID
     * @param message 要发送的消息内容
     */
    public void sendMessageToUser(String userId, String message) {
        // 根据用户ID获取对应的WebSocket会话
        WebSocketSession session = userSessions.get(userId);

        // 检查会话是否存在且连接状态正常
        if (session != null && session.isOpen()) {
            try {
                // 发送文本消息
                session.sendMessage(new TextMessage(message));
                log.info("消息已发送给用户 {}: {}", userId, message);
            } catch (IOException e) {
                log.error("发送消息给用户 {} 失败", userId, e);
            }
        } else {
            // 用户不在线或连接已断开的情况
            log.warn("用户 {} 不在线或连接已关闭", userId);
        }
    }

    /**
     * 向所有在线用户广播消息（多播）
     * <p>
     * 遍历所有在线用户的WebSocket会话，向每个用户发送相同的消息。
     * 这是群发消息的实现，常用于系统公告、全员通知等场景。
     * 只向连接状态正常的用户发送消息，忽略已断开的连接。
     *
     * @param message 要广播的消息内容
     */
    public void broadcastMessage(String message) {
        // 遍历所有在线用户会话
        userSessions.forEach((userId, session) -> {
            // 检查连接状态，只向正常连接的用户发送消息
            if (session.isOpen()) {
                try {
                    session.sendMessage(new TextMessage(message));
                } catch (IOException e) {
                    log.error("广播消息给用户 {} 失败", userId, e);
                }
            }
        });
        log.info("消息已广播给 {} 个用户: {}", userSessions.size(), message);
    }

    /**
     * 向所有在线用户广播消息（排除发送者）
     * <p>
     * 向除了消息发送者之外的所有在线用户广播消息。
     * 这是聊天室消息转发的核心实现，避免消息发送者收到自己发送的消息。
     * 常用于群聊消息转发、用户状态变更通知等场景。
     *
     * @param message       要广播的消息内容
     * @param senderSession 消息发送者的WebSocket会话，将被排除在广播范围外
     */
    public void broadcastMessageExcludeSender(String message, WebSocketSession senderSession) {
        int sentCount = 0;  // 记录实际发送成功的用户数量

        // 遍历所有在线用户会话
        for (Map.Entry<String, WebSocketSession> entry : userSessions.entrySet()) {
            WebSocketSession session = entry.getValue();

            // 检查连接状态且排除发送者自己
            if (session.isOpen() && !session.equals(senderSession)) {
                try {
                    session.sendMessage(new TextMessage(message));
                    sentCount++;  // 发送成功计数
                } catch (IOException e) {
                    log.error("广播消息给用户 {} 失败", entry.getKey(), e);
                }
            }
        }
        log.info("消息已广播给 {} 个用户（排除发送者）: {}", sentCount, message);
    }
}
